在前面二篇,我們把 Redux API 的重點 Store、Actions、Reducer 都介紹完成了,也用範例來示範了建置的方式,接下來就要示範 React Compoent 要如何去使用 Redux 來完成狀態管理。
這個在前面介紹 Store 時有提到,元件要使用 Store,就必需使用 Provider 語法包住元件,並指定 Store 給 Provider。
// src/index.js
import { Provider } from 'react-redux';
import { App } from './App';
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
在元件內使用 Redux,就需要用到二個 React Hook 如下
useSelector Hook Function 需要傳入一個參數,這個參數也是 Function,在這個 Function 裡定義要如何從所有 state 中挑選需要的 state
import { useSelector } from 'react-redux';
const filter = useSelector((state) => state.filter);
const todos = useSelector((state) => state.todos);
使用 useDispatch 取得 dispatch 後,就可以套件前面做好的 Action Creators Function,讓事件透過 dispatch 發出 Action 及 Payload。
import { useRef } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from 'store/actins';
const TodoTextInput = () => {
const dispatch = useDispatch();
const inputRef = useRef(null);
const onSubmitHandler = (e) => {
if (e.which === 13) {
if (!inputRef.current.value.trim()) {
return;
}
dispatch(addTodo(inputRef.current.value));
inputRef.current.value = "";
}
};
return (
<div>
<input
type="text"
ref={inputRef}
onKeyDown={onSubmitHandler}
/>
</div>
);
}
前面的程式碼片段,比較偏重擷取重點呈現的部分,幫助大家一點一滴的去理解應用程式如何套用 Redux,在熟悉了整個 Redux 操作後,這邊提供了一個 CodeSandbox 模版,讓大家試著從頭開始建立 React Todo MVC with Redux。
元件的相依性示意如下,如果要共享狀態,要層層傳遞 props
CodeSandbox 模版如下
https://codesandbox.io/s/react-todomvc-usestate-rjbu0w
你可以 Fork 一份,來試看看製作 Redux 的版本
建立 Store 元件,並將 reducers 與其綁定
import { createStore } from "redux";
import reducers from "./reducers";
const store = createStore(reducers);
export default store;
export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const DELETE_TODO = "DELETE_TODO";
export const SET_FILTER = "SET_FILTER";
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER } from "./actionTypes";
export const addTodo = (text) => {
return {
type: ADD_TODO,
id: new Date().getTime().toString(),
text
};
};
export const toggleTodo = (id) => {
return {
type: TOGGLE_TODO,
id
};
};
export const deleteTodo = (id) => {
return {
type: DELETE_TODO,
id
};
};
export const setFilter = (filter) => {
return {
type: SET_FILTER,
filter
};
};
import { combineReducers } from "redux";
import todos from "./todosReducer";
import filter from "./filterReducer";
const reducers = combineReducers({
todos,
filter
});
export default reducers;
用來對應與 todos state 相關的 action
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO } from "./../actions/actionTypes";
const todosReducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case TOGGLE_TODO:
return state.map((todo) => {
if (todo.id !== action.id) {
return todo;
}
return Object.assign({}, todo, {
completed: !todo.completed
});
});
case DELETE_TODO:
return state.filter((todo) => {
return todo.id !== action.id;
});
default:
return state;
}
};
export default todosReducer;
用來對應與 filter state 相關的 action
import { SET_FILTER } from "./../actions/actionTypes";
const filterReducer = (state = "All", action) => {
switch (action.type) {
case SET_FILTER:
return action.filter;
default:
return state;
}
};
export default filterReducer;
在此使用 Provider 為整個專案加上 Store
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import store from "./store";
import "todomvc-app-css/index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
import React from "react";
import Header from "./Header";
import Footer from "./Footer";
import TodoList from "./TodoList";
const App = () => {
return (
<div>
<Header />
<TodoList />
<Footer />
</div>
);
};
export default App;
Header 元件
import React, { useRef } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../store/actions";
import TodoTextInput from "./TodoTextInput";
const Header = () => {
const dispatch = useDispatch();
const inputRef = useRef(null);
const onSubmitHandler = (e) => {
if (e.which === 13) {
if (!inputRef.current.value.trim()) {
return;
}
dispatch(addTodo(inputRef.current.value));
inputRef.current.value = "";
}
};
return (
<header className="header">
<h1>todos</h1>
<TodoTextInput
ref={inputRef}
placeholder="What needs to be done?"
onSubmit={onSubmitHandler}
/>
</header>
);
};
export default Header;
Header 裡包含的 TodoTextInput 元件
這邊使用了 forwardRef 把 原生 input 的參考曝露出去,讓上層元件操作
把對 Redux 的操作放在 專屬於應用程式的 Container 類型元件是比較好的作法
這樣基礎元件能夠被其他元件重用性會更高。
import React, { forwardRef } from "react";
const TodoTextInput = forwardRef((props, ref) => {
const { placeholder, onSubmit } = props;
return (
<div>
<input
type="text"
className="new-todo"
ref={ref}
placeholder={placeholder}
onKeyDown={onSubmit}
/>
</div>
);
});
export default TodoTextInput;
TodoList 元件
import React from "react";
import TodoItem from "./TodoItem";
import { useDispatch, useSelector } from "react-redux";
import { toggleTodo, deleteTodo } from "../store/actions";
const TodoList = () => {
const todos = useSelector((state) => state.todos);
const filter = useSelector((state) => state.filter);
const dispatch = useDispatch();
// 對應狀態的TodoList
const filteredTodos = todos.filter((item) => {
if (filter === "Completed") {
return item.completed;
}
if (filter === "Active") {
return !item.completed;
}
return true;
});
return (
<>
<section className="main">
<ul className="todo-list">
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggleItem={() => dispatch(toggleTodo(todo.id))}
onDeleteItem={() => dispatch(deleteTodo(todo.id))}
/>
))}
</ul>
</section>
</>
);
};
export default TodoList;
TodoList 下的 TodoItem 元件
import React from "react";
import classnames from "classnames";
const TodoItem = ({ onToggleItem, onDeleteItem, todo }) => {
const { text, completed } = todo;
return (
<li
className={classnames({
completed: completed
})}
>
<div className="view">
<input className="toggle" type="checkbox" checked={completed} onChange={onToggleItem} />
<label>{text}</label>
<button className="destroy" onClick={onDeleteItem} />
</div>
</li>
);
};
export default TodoItem;
Footer 元件 用來切換 Filter
import React from "react";
import classnames from "classnames";
import { useDispatch, useSelector } from "react-redux";
import { setFilter } from "../store/actions";
const Footer = (props) => {
const todos = useSelector((state) => state.todos);
const filter = useSelector((state) => state.filter);
const dispatch = useDispatch();
// 待完成的TodoList
const activeTodos = todos.filter((item) => !item.completed);
const itemWord = activeTodos.length === 1 ? "item" : "items";
const FILTER_TITLES = ["All", "Active", "Completed"];
return (
<footer className="footer">
<span className="todo-count">
<strong>{activeTodos.length || "No"}</strong> {itemWord} left
</span>
<ul className="filters">
{FILTER_TITLES.map((filterTitle) => (
<li key={filterTitle}>
<a
className={classnames({ selected: filterTitle === filter })}
style={{ cursor: "pointer" }}
onClick={() => dispatch(setFilter(filterTitle))}
>
{filterTitle}
</a>
</li>
))}
</ul>
</footer>
);
};
export default Footer;
完成後的 CodeSandbox 連結如下
https://codesandbox.io/s/react-todomvc-redux-6qnps3
可以觀察 Redux 與元件的關係,只要把 Store 提供給 APP,在任何層級的元件,就可以使用 useSelector 取得 Store 裡的任意 state 來使用,同時 state 也只會用預先定義好的 Action(規則) 做更新。
selector 會辨識元件選取的 state 是否有被更新,所以 todos 更新,不會造成 filter 相關的元件被 Re-Render。
這邊裡我們的 TodoTextInput 及 TodoItem 元件的資料,還是透過 props 傳入,因為我們會希望這種只是展現 UI 的基礎元件是能夠被其他上層元件重複使用組合的,所以讓它們與 Redux 連結並不合適。
TodoTextInput 及 TodoItem 是 Presentational Component
,而 Header、TodoList 及 Footer 則歸類為 Container Components
。
Presentational Component 主要負責單純的 UI 的渲染,而 Container 則負責和 Redux 的 Store 溝通。這樣的分法可以讓程式架構和職責更清楚。
開發者要正確的使用 Redux,就要先掌握 Store、Action、Reducer 這些基本概念,但如果想進一步透過 Redux 處理非同步、API 請求等進階需求,就要學會 Redux Middleware。
接下來會先介紹 Redux Middleware 的基本觀念,再來介紹 Redux 衍生出來的生態系套件。
https://www.freecodecamp.org/news/what-is-redux-store-actions-reducers-explained/
https://www.laitimes.com/article/1od0a_1u849.html
https://chentsulin.github.io/redux/index.html
https://pjchender.dev/webdev/note-without-redux/
https://ithelp.ithome.com.tw/articles/10219962
https://github.com/kdchang/reactjs101/blob/master/Ch08/container-presentational-component-.md